# 函数式编程
# 函数是Python内建支持的一种封装，我们通过把大段代码拆成函数，通过一层一层的函数调用，就可以把复杂任务分解成简单的任务，
# 这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
# 而函数式编程（请注意多了一个“式”字）——Functional Programming，虽然也可以归结到面向过程的程序设计，但其思想更接近数学计算。
# 越低级的语言，越贴近计算机，抽象程度低，执行效率高，比如C语言；越高级的语言，越贴近计算，抽象程度高，执行效率低，比如Lisp语言
# 函数式编程就是一种抽象程度很高的编程范式，纯粹的函数式编程语言编写的函数没有变量，因此，任意一个函数，只要输入是确定的，
# 输出就是确定的，这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言，由于函数内部的变量状态不确定，同样的输入，
# 可能得到不同的输出，因此，这种函数是有副作用的。
# 函数式编程的一个特点就是，允许把函数本身作为参数传入另一个函数，还允许返回一个函数！
# Python对函数式编程提供部分支持。由于Python允许使用变量，因此，Python不是纯函数式编程语言。

# 高阶函数
# 1. 变量可以指向函数
x = abs(-10)  # 获得函数调用结果赋值给变量
print(x)
y = abs  # 把函数本身赋值给变量
print(y(-10))


# 2. 函数名也是变量
# 函数名其实就是指向函数的变量！对于abs()这个函数，完全可以把函数名abs看成变量，它指向一个可以计算绝对值的函数！
# abs = 8
# try:
#     print(abs(-10))
# except TypeError as e:
#     print('TypeError:', e.args)


# 3. 传入函数
# 既然变量可以指向函数，函数的参数能接收变量，那么一个函数就可以接收另一个函数作为参数，这种函数就称之为高阶函数。
def add(x, y, f):
    return f(x) + f(y)


n = add(-5, 8, abs)
print(n)


# 4. map/reduce
# map()函数接收两个参数，一个是函数，一个是Iterable，map将传入的函数依次作用到序列的每个元素，并把结果作为新的Iterator返回
def f(x):
    return x * x


r = map(f, [1, 2, 3, 4, 5])
print(list(r))
# 上面也可以用for循环实现,，但能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗？
# 所以，map()作为高阶函数，事实上它把运算规则抽象了

# reduce把一个函数作用在一个序列[x1, x2, x3, ...]上，这个函数必须接收两个参数，reduce把结果继续和序列的下一个元素做累积计算
from functools import reduce


def add(x, y):
    return x * 10 + y


n = reduce(add, [1, 2, 3, 4])
print(n)

# 组合使用
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}


def str2int(s):
    def fn(x, y):
        return x * 10 + y

    def char2num(s):
        return DIGITS[s]

    return reduce(fn, map(char2num, s))


s = '12434'
print(s)
print(str2int(s))


# 使用lambda简化
def char2num(s):
    return DIGITS[s]


def str2int2(s):
    return reduce(lambda x, y: x * 10 + y, map(char2num, s))


print(str2int2(s))


# 5.filter
# Python内建的filter()函数用于过滤序列。
# 和map()类似，也接收一个函数和一个序列。不同的是，filter()把传入的函数依次作用于每个元素，然后根据返回值是True还是False决定保留还是丢弃该元素。
def is_odd(n):
    return n % 2 == 1


l = list(filter(is_odd, [1, 2, 3, 4, 5, 6, 7]))
print(l)


def not_empty(s):
    return s and s.strip()


l2 = list(filter(not_empty, ['a', '', None, 'haha', ' ']))
print(l2)
# 注意: filter()函数返回的是一个Iterator，也就是一个惰性序列，所以要强迫filter()完成计算结果，需要用list()函数获得所有结果并返回list。

# 6. sorted
# Python内置的sorted()函数就可以对list进行排序：
print(sorted([2, 1, -10, -1, 22]))
# 此外，sorted()函数也是一个高阶函数，它还可以接收一个key函数来实现自定义的排序，例如按绝对值大小排序：
print(sorted([2, 1, -10, -1, 22], key=abs))
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower))
# 要进行反向排序，不必改动key函数，可以传入第三个参数reverse=True：
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True))

# 返回函数
# 番外：刚在一个开发群里看到讨论交换两个变量值的蕞简写发
a = 123
b = 456
print('a=', a)
print('b=', b)
a = a ^ b
b = a ^ b
a = a ^ b
print('a=', a)
print('b=', b)
a, b = b, a
print('a=', a)
print('b=', b)


# 高阶函数除了可以接受函数作为参数外，还可以把函数作为结果值返回。
# 如果不需要立刻求和，而是在后面的代码中，根据需要再计算怎么办？可以不返回求和的结果，而是返回求和的函数：

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax += n
        return ax

    return sum


# 当我们调用lazy_sum()时，返回的并不是求和结果，而是求和函数：
f = lazy_sum(1, 2, 3, 4, 5)
print(f)
# 调用函数f时，才真正计算求和的结果：
print(f())
# 我们在函数lazy_sum中又定义了函数sum，并且，内部函数sum可以引用外部函数lazy_sum的参数和局部变量，当lazy_sum返回函数sum时，
# 相关参数和变量都保存在返回的函数中，这种称为“闭包（Closure）”的程序结构拥有极大的威力。
# 请再注意一点，当我们调用lazy_sum()时，每次调用都会返回一个新的函数，即使传入相同的参数：
f2 = lazy_sum(1, 2, 3, 4, 5)
print(f == f2)


# 闭包
# 注意到返回的函数在其定义内部引用了局部变量args，所以，当一个函数返回了一个函数后，其内部的局部变量还被新函数引用，所以，闭包用起来简单，实现起来可不容易。
# 另一个需要注意的问题是，返回的函数并没有立刻执行，而是直到调用了f()才执行。我们来看一个例子：
def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i * i

        fs.append(f)
    return fs


f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())


# 在上面的例子中，每次循环，都创建了一个新的函数，然后，把创建的3个函数都返回了。
# 返回的函数引用了变量i，但它并非立刻执行。等到3个函数都返回时，它们所引用的变量i已经变成了3，因此最终结果为9
# 如果一定要引用循环变量怎么办？方法是再创建一个函数，用该函数的参数绑定循环变量当前的值，无论该循环变量后续如何更改，已绑定到函数参数的值不变：
def count2():
    def f(i):
        def g():
            return i * i

        return g

    fs = []
    for i in range(1, 4):
        fs.append(f(i))
    return fs


f1, f2, f3 = count2()
print(f1())
print(f2())
print(f3())

# 匿名函数
# 当我们在传入函数时，有些时候，不需要显式地定义函数，直接传入匿名函数更方便。
# 关键字lambda表示匿名函数
f = lambda x: x * x
print(f(5))


# 也可以把匿名函数作为返回值返回
def build(x, y):
    return lambda: x * x + y * y


print(build(3, 4))
print(build(3, 4)())


# 装饰器
# 由于函数也是一个对象，而且函数对象可以被赋值给变量，所以，通过变量也能调用该函数。
def now():
    print('2019.08.14')


f = now
print(f)
print(f())
# 函数对象有一个__name__属性，可以拿到函数的名字：
print(f.__name__)


# 现在，假设我们要增强now()函数的功能，比如，在函数调用前后自动打印日志，但又不希望修改now()函数的定义，
# 这种在代码运行期间动态增加功能的方式，称之为“装饰器”（Decorator）
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)

    return wrapper


# 我们要借助Python的@语法，把decorator置于函数的定义处：
@log
def now2():
    print('2019.08.14')


now2()
# 相当于:
now = log(now)
now()


# 如果decorator本身需要传入参数，那就需要编写一个返回decorator的高阶函数，写出来会更复杂。比如，要自定义log的文本：
def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s()' % (text, func.__name__))
            return func(*args, **kw)

        return wrapper

    return decorator


@log('执行的函数为')
def now3():
    print('2019.08.14')


now3()
# 相当于:
abs = log('执行的函数为')(abs)
print(abs((-10)))
print(abs.__name__)  # 看经过decorator装饰之后的函数，它们的__name__已经从原来的'now'变成了'wrapper'：
# 因为返回的那个wrapper()函数名字就是'wrapper'，所以，需要把原始函数的__name__等属性复制到wrapper()函数中，否则，有些依赖函数签名的代码执行就会出错。
# 不需要编写wrapper.__name__ = func.__name__这样的代码，Python内置的functools.wraps就是干这个事的，所以，一个完整的decorator的写法如下：
import functools


def log2(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s()' % (text, func.__name__))
            return func(*args, **kw)

        return wrapper

    return decorator


@log2('执行的函数为')
def now4():
    print('2019.08.14')


now4()
print(now4.__name__)

# 偏函数, functools.partial

print(int('11'))
print(int('11', base=8))
print(int('11', 16))
import functools

int2 = functools.partial(int, base=2)
print(int2('11'))


# 相当于:
def int2(x, base=2):
    return int(x, base)


# 也相当于这样写
kw = {'base': 2}
print(int('11', **kw))

# 又如:
max2 = functools.partial(max, 10)
print(max2(2, 3, 4))
# 实际上会把10作为*args的一部分自动加到左边，也就是：
args = (10, 2, 3, 4)
print(max(*args))
